"""
libclang integration wrapper for C++ AST parsing.

Provides a high-level interface to libclang for parsing C++ source files
and extracting AST information with proper error handling and resource management.
"""

import os
import sys
from typing import Dict, List, Any, Optional, Set, Tuple, Union
from pathlib import Path
from dataclasses import dataclass
import logging

try:
    import clang.cindex as clang
    from clang.cindex import (
        Index, TranslationUnit, Cursor, CursorKind, TypeKind, 
        SourceLocation, SourceRange, Diagnostic, Config
    )
    LIBCLANG_AVAILABLE = True
except ImportError:
    LIBCLANG_AVAILABLE = False
    # Create dummy classes for type hints when libclang is not available
    class Index: pass
    class TranslationUnit: pass
    class Cursor: pass
    class CursorKind: pass
    class TypeKind: pass
    class SourceLocation: pass
    class SourceRange: pass
    class Diagnostic: pass

from ...core.error_handler import AnalysisError, log_info, log_debug


@dataclass
class CompilationConfig:
    """Configuration for C++ compilation."""
    standard: str = "c++17"
    include_paths: List[str] = None
    defines: Dict[str, str] = None
    system_includes: List[str] = None
    flags: List[str] = None
    
    def __post_init__(self):
        if self.include_paths is None:
            self.include_paths = []
        if self.defines is None:
            self.defines = {}
        if self.system_includes is None:
            self.system_includes = []
        if self.flags is None:
            self.flags = []


@dataclass
class ParseResult:
    """Result of parsing a C++ file."""
    file_path: str
    translation_unit: Any  # TranslationUnit
    diagnostics: List[Dict[str, Any]]
    success: bool
    error_message: Optional[str] = None


class ClangWrapper:
    """High-level wrapper around libclang for C++ parsing."""
    
    def __init__(self, config: CompilationConfig = None):
        """Initialize the clang wrapper."""
        if not LIBCLANG_AVAILABLE:
            raise AnalysisError(
                "libclang is not available. Please install python-clang bindings."
            )
        
        self.config = config or CompilationConfig()
        self.index = None
        self._setup_clang()
    
    def _setup_clang(self):
        """Setup libclang configuration."""
        try:
            # Try to find libclang automatically
            Config.set_compatibility_check(False)
            
            # Initialize index
            self.index = Index.create()
            
            log_info("libclang initialized successfully")
            
        except Exception as e:
            raise AnalysisError(f"Failed to initialize libclang: {e}")
    
    def _build_compile_args(self, file_path: str) -> List[str]:
        """Build compilation arguments for libclang."""
        args = []
        
        # C++ standard
        args.append(f"-std={self.config.standard}")
        
        # Include paths
        for include_path in self.config.include_paths:
            args.extend(["-I", include_path])
        
        # System includes
        for sys_include in self.config.system_includes:
            args.extend(["-isystem", sys_include])
        
        # Defines
        for name, value in self.config.defines.items():
            if value:
                args.append(f"-D{name}={value}")
            else:
                args.append(f"-D{name}")
        
        # Additional flags
        args.extend(self.config.flags)
        
        # Common C++ flags for better parsing
        args.extend([
            "-x", "c++",  # Force C++ mode
            "-w",         # Suppress warnings (for cleaner parsing)
        ])
        
        return args
    
    def parse_file(self, file_path: str, unsaved_files: Dict[str, str] = None) -> ParseResult:
        """
        Parse a C++ file using libclang.
        
        Args:
            file_path: Path to the C++ file to parse
            unsaved_files: Dictionary mapping file paths to unsaved content
            
        Returns:
            ParseResult containing the translation unit and diagnostics
        """
        if not Path(file_path).exists():
            return ParseResult(
                file_path=file_path,
                translation_unit=None,
                diagnostics=[],
                success=False,
                error_message=f"File not found: {file_path}"
            )
        
        try:
            args = self._build_compile_args(file_path)
            
            log_debug(f"Parsing C++ file: {file_path}")
            log_debug(f"Compile args: {' '.join(args)}")
            
            # Prepare unsaved files for libclang
            unsaved_files_list = []
            if unsaved_files:
                for path, content in unsaved_files.items():
                    unsaved_files_list.append((path, content))
            
            # Parse the file
            translation_unit = self.index.parse(
                file_path,
                args=args,
                unsaved_files=unsaved_files_list,
                options=(
                    TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
                    TranslationUnit.PARSE_SKIP_FUNCTION_BODIES |
                    TranslationUnit.PARSE_INCLUDE_BRIEF_COMMENTS_IN_CODE_COMPLETION
                )
            )
            
            # Process diagnostics
            diagnostics = []
            has_errors = False
            
            for diagnostic in translation_unit.diagnostics:
                diag_info = {
                    'severity': diagnostic.severity,
                    'location': {
                        'file': diagnostic.location.file.name if diagnostic.location.file else None,
                        'line': diagnostic.location.line,
                        'column': diagnostic.location.column,
                        'offset': diagnostic.location.offset
                    },
                    'spelling': diagnostic.spelling,
                    'ranges': [],
                    'fixits': []
                }
                
                # Extract ranges
                for range_info in diagnostic.ranges:
                    diag_info['ranges'].append({
                        'start': {
                            'line': range_info.start.line,
                            'column': range_info.start.column
                        },
                        'end': {
                            'line': range_info.end.line,
                            'column': range_info.end.column
                        }
                    })
                
                # Extract fix-its
                for fixit in diagnostic.fixits:
                    diag_info['fixits'].append({
                        'range': {
                            'start': {
                                'line': fixit.range.start.line,
                                'column': fixit.range.start.column
                            },
                            'end': {
                                'line': fixit.range.end.line,
                                'column': fixit.range.end.column
                            }
                        },
                        'replacement': fixit.value
                    })
                
                diagnostics.append(diag_info)
                
                # Check for fatal errors
                if diagnostic.severity >= Diagnostic.Error:
                    has_errors = True
            
            return ParseResult(
                file_path=file_path,
                translation_unit=translation_unit,
                diagnostics=diagnostics,
                success=not has_errors,
                error_message=None if not has_errors else "Parse errors encountered"
            )
            
        except Exception as e:
            log_debug(f"Error parsing {file_path}: {e}")
            return ParseResult(
                file_path=file_path,
                translation_unit=None,
                diagnostics=[],
                success=False,
                error_message=str(e)
            )
    
    def get_cursor_info(self, cursor: Cursor) -> Dict[str, Any]:
        """Extract detailed information from a libclang cursor."""
        if not cursor:
            return {}
        
        try:
            location = cursor.location
            extent = cursor.extent
            
            info = {
                'kind': cursor.kind.name if cursor.kind else 'unknown',
                'spelling': cursor.spelling or '',
                'display_name': cursor.displayname or '',
                'usr': cursor.get_usr() or '',
                'location': {
                    'file': location.file.name if location.file else None,
                    'line': location.line,
                    'column': location.column,
                    'offset': location.offset
                },
                'extent': {
                    'start': {
                        'line': extent.start.line,
                        'column': extent.start.column
                    },
                    'end': {
                        'line': extent.end.line,
                        'column': extent.end.column
                    }
                } if extent else None,
                'type': self.get_type_info(cursor.type) if cursor.type else {},
                'is_definition': cursor.is_definition(),
                'is_virtual_method': cursor.is_virtual_method(),
                'is_pure_virtual_method': cursor.is_pure_virtual_method(),
                'is_static_method': cursor.is_static_method(),
                'access_specifier': cursor.access_specifier.name if hasattr(cursor, 'access_specifier') else None,
                'storage_class': cursor.storage_class.name if hasattr(cursor, 'storage_class') else None,
                'linkage': cursor.linkage.name if hasattr(cursor, 'linkage') else None,
                'availability': cursor.availability.name if hasattr(cursor, 'availability') else None,
                'language': cursor.language.name if hasattr(cursor, 'language') else None
            }
            
            # Add template information if available
            if hasattr(cursor, 'get_template_argument_kind'):
                try:
                    template_args = []
                    for i in range(cursor.get_num_template_arguments()):
                        arg_kind = cursor.get_template_argument_kind(i)
                        if arg_kind == clang.TemplateArgumentKind.TYPE:
                            arg_type = cursor.get_template_argument_type(i)
                            template_args.append({
                                'kind': 'type',
                                'type': self.get_type_info(arg_type)
                            })
                        elif arg_kind == clang.TemplateArgumentKind.INTEGRAL:
                            template_args.append({
                                'kind': 'integral',
                                'value': cursor.get_template_argument_value(i)
                            })
                    info['template_arguments'] = template_args
                except:
                    pass
            
            # Add enum value if it's an enum constant
            if cursor.kind == CursorKind.ENUM_CONSTANT_DECL:
                try:
                    info['enum_value'] = cursor.enum_value
                except:
                    pass
            
            return info
            
        except Exception as e:
            log_debug(f"Error extracting cursor info: {e}")
            return {'error': str(e)}
    
    def get_type_info(self, type_obj) -> Dict[str, Any]:
        """Extract information from a libclang type object."""
        if not type_obj:
            return {}
        
        try:
            info = {
                'spelling': type_obj.spelling or '',
                'kind': type_obj.kind.name if type_obj.kind else 'unknown',
                'is_const_qualified': type_obj.is_const_qualified(),
                'is_volatile_qualified': type_obj.is_volatile_qualified(),
                'is_restrict_qualified': type_obj.is_restrict_qualified(),
                'is_pod': type_obj.is_pod(),
                'size': -1,
                'align': -1
            }
            
            # Try to get size and alignment (may fail for incomplete types)
            try:
                info['size'] = type_obj.get_size()
                info['align'] = type_obj.get_align()
            except:
                pass
            
            # Get pointee type for pointer types
            if type_obj.kind in (TypeKind.POINTER, TypeKind.LVALUEREFERENCE, TypeKind.RVALUEREFERENCE):
                try:
                    pointee = type_obj.get_pointee()
                    info['pointee'] = self.get_type_info(pointee)
                except:
                    pass
            
            # Get array element type and size
            if type_obj.kind == TypeKind.CONSTANTARRAY:
                try:
                    element_type = type_obj.get_array_element_type()
                    info['element_type'] = self.get_type_info(element_type)
                    info['array_size'] = type_obj.get_array_size()
                except:
                    pass
            
            # Get function signature for function types
            if type_obj.kind == TypeKind.FUNCTIONPROTO:
                try:
                    result_type = type_obj.get_result()
                    info['result_type'] = self.get_type_info(result_type)
                    
                    arg_types = []
                    for i in range(type_obj.get_num_arg_types()):
                        arg_type = type_obj.get_arg_type(i)
                        arg_types.append(self.get_type_info(arg_type))
                    info['argument_types'] = arg_types
                    
                    info['is_variadic'] = type_obj.is_function_variadic()
                except:
                    pass
            
            return info
            
        except Exception as e:
            log_debug(f"Error extracting type info: {e}")
            return {'error': str(e)}
    
    def find_definition(self, cursor: Cursor) -> Optional[Cursor]:
        """Find the definition of a cursor (if it's a declaration)."""
        try:
            definition = cursor.get_definition()
            return definition if definition and definition != cursor else None
        except:
            return None
    
    def find_references(self, cursor: Cursor, translation_unit: TranslationUnit) -> List[Cursor]:
        """Find all references to a cursor within a translation unit."""
        references = []
        
        def visit_node(node):
            try:
                if node.referenced and node.referenced == cursor:
                    references.append(node)
                
                # Continue traversing
                for child in node.get_children():
                    visit_node(child)
            except:
                pass
        
        try:
            visit_node(translation_unit.cursor)
        except Exception as e:
            log_debug(f"Error finding references: {e}")
        
        return references
    
    def get_included_files(self, translation_unit: TranslationUnit) -> List[Dict[str, Any]]:
        """Get all files included by a translation unit."""
        included_files = []
        
        try:
            for include in translation_unit.get_includes():
                included_files.append({
                    'file': include.include.name if include.include else None,
                    'location': {
                        'file': include.location.file.name if include.location.file else None,
                        'line': include.location.line,
                        'column': include.location.column
                    },
                    'depth': include.depth,
                    'is_input_file': include.is_input_file
                })
        except Exception as e:
            log_debug(f"Error getting included files: {e}")
        
        return included_files
    
    def cleanup(self):
        """Clean up resources."""
        # libclang handles cleanup automatically
        pass
    
    def __enter__(self):
        """Context manager entry."""
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.cleanup()